AWS WAF v2のS3へのログ出力をAWS CDK(一部AWS CLI)で設定してみた
こんにちは、CX事業本部の若槻です。
前回のエントリでは、AWS WAFv2のWebACLの作成とCloudFrontへの適用をAWS CDKで行いました。
このAWS WAFv2にはログ出力の設定がありますが、そのためには別途FirehoseでDeliveryStreamを作成して紐付ける必要があります。
今回は、AWS WAF v2のFirehoseを使用したS3へのログ出力をAWS CDK(一部AWS CLI)で設定してみました。
やってみた
前回エントリで作成したリソースに修正を加える形で手順をご紹介します。
CDKモジュール追加
% npm i @aws-cdk/aws-kinesisfirehose @aws-cdk/aws-logs
CDKスタックへのリソース追加
ハイライト箇所が追記部分です。
import * as cdk from "@aws-cdk/core"; import * as cloudfront from "@aws-cdk/aws-cloudfront"; import * as s3 from "@aws-cdk/aws-s3"; import * as s3deploy from "@aws-cdk/aws-s3-deployment"; import * as iam from "@aws-cdk/aws-iam"; import * as wafv2 from "@aws-cdk/aws-wafv2"; import * as firehose from "@aws-cdk/aws-kinesisfirehose"; import * as logs from "@aws-cdk/aws-logs"; export class AwsCdkDeployReactStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const websiteBucket = new s3.Bucket(this, "WebsiteBucket", { websiteErrorDocument: "index.html", websiteIndexDocument: "index.html", }); const websiteIdentity = new cloudfront.OriginAccessIdentity( this, "WebsiteIdentity" ); const webSiteBucketPolicyStatement = new iam.PolicyStatement({ actions: ["s3:GetObject"], effect: iam.Effect.ALLOW, principals: [websiteIdentity.grantPrincipal], resources: [`${websiteBucket.bucketArn}/*`], }); websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement); //AWSマネージドルールを適用するWebACLの作成 const websiteWafV2WebAcl = new wafv2.CfnWebACL(this, "WafV2WebAcl", { defaultAction: { allow: {} }, scope: "CLOUDFRONT", visibilityConfig: { cloudWatchMetricsEnabled: true, sampledRequestsEnabled: true, metricName: "websiteWafV2WebAcl", }, rules: [ { name: "AWSManagedRulesCommonRuleSet", priority: 1, statement: { managedRuleGroupStatement: { vendorName: "AWS", name: "AWSManagedRulesCommonRuleSet", }, }, overrideAction: { none: {} }, visibilityConfig: { cloudWatchMetricsEnabled: true, sampledRequestsEnabled: true, metricName: "AWSManagedRulesCommonRuleSet", }, }, { name: "AWSManagedRulesAdminProtectionRuleSet", priority: 2, statement: { managedRuleGroupStatement: { vendorName: "AWS", name: "AWSManagedRulesAdminProtectionRuleSet", }, }, overrideAction: { none: {} }, visibilityConfig: { cloudWatchMetricsEnabled: true, sampledRequestsEnabled: true, metricName: "AWSManagedRulesAdminProtectionRuleSet", }, }, { name: "AWSManagedRulesKnownBadInputsRuleSet", priority: 3, statement: { managedRuleGroupStatement: { vendorName: "AWS", name: "AWSManagedRulesKnownBadInputsRuleSet", }, }, overrideAction: { none: {} }, visibilityConfig: { cloudWatchMetricsEnabled: true, sampledRequestsEnabled: true, metricName: "AWSManagedRulesKnownBadInputsRuleSet", }, }, { name: "AWSManagedRulesAmazonIpReputationList", priority: 4, statement: { managedRuleGroupStatement: { vendorName: "AWS", name: "AWSManagedRulesAmazonIpReputationList", }, }, overrideAction: { none: {} }, visibilityConfig: { cloudWatchMetricsEnabled: true, sampledRequestsEnabled: true, metricName: "AWSManagedRulesAmazonIpReputationList", }, }, { name: "AWSManagedRulesAnonymousIpList", priority: 5, statement: { managedRuleGroupStatement: { vendorName: "AWS", name: "AWSManagedRulesAnonymousIpList", }, }, overrideAction: { none: {} }, visibilityConfig: { cloudWatchMetricsEnabled: true, sampledRequestsEnabled: true, metricName: "AWSManagedRulesAnonymousIpList", }, }, ], }); const websiteDistribution = new cloudfront.CloudFrontWebDistribution( this, "WebsiteDistribution", { errorConfigurations: [ { errorCachingMinTtl: 300, errorCode: 403, responseCode: 200, responsePagePath: "/index.html", }, { errorCachingMinTtl: 300, errorCode: 404, responseCode: 200, responsePagePath: "/index.html", }, ], originConfigs: [ { s3OriginSource: { s3BucketSource: websiteBucket, originAccessIdentity: websiteIdentity, }, behaviors: [ { isDefaultBehavior: true, }, ], }, ], priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL, webACLId: websiteWafV2WebAcl.attrArn, //作成したWebACLをCloudFrontに適用する } ); new s3deploy.BucketDeployment(this, "WebsiteDeploy", { sources: [s3deploy.Source.asset("./web/build")], destinationBucket: websiteBucket, distribution: websiteDistribution, distributionPaths: ["/*"], }); const region = cdk.Stack.of(this).region; const accountId = cdk.Stack.of(this).account; const deliveryStreamName = "aws-waf-logs-demo"; const logStreamName = "S3Delivery"; //データ配信先S3バケット const wafLogBucket = new s3.Bucket(this, "wafLogBucket", { bucketName: `${deliveryStreamName}-${region}-${accountId}`, }); //データ配信失敗時のイベントを記録するLogGroupとLogStream const wafLogDeliveryStreamLogGroup = new logs.CfnLogGroup( this, "wafLogDeliveryStreamLogGroup", { logGroupName: `/aws/kinesisfirehose/${deliveryStreamName}`, } ); const wafLogDeliveryStreamLogStream = new logs.CfnLogStream( this, "wafLogDeliveryStreamLogStream", { logGroupName: wafLogDeliveryStreamLogGroup.logGroupName as string, logStreamName: logStreamName, } ); //明示的に依存性を設定しなければLogStreamの作成が失敗する場合がある wafLogDeliveryStreamLogStream.addDependsOn(wafLogDeliveryStreamLogGroup); //配信ストリームに付与するロール const wafLogDeliveryStreamRole = new iam.Role( this, "wafLogDeliveryStreamRole", { assumedBy: new iam.ServicePrincipal("firehose.amazonaws.com"), } ); wafLogDeliveryStreamRole.addToPolicy( new iam.PolicyStatement({ actions: [ "kinesis:DescribeStream", "kinesis:GetShardIterator", "kinesis:GetRecords", ], effect: iam.Effect.ALLOW, resources: [`arn:aws:kinesis:${region}:${accountId}:stream/*`], }) ); wafLogDeliveryStreamRole.addToPolicy( new iam.PolicyStatement({ actions: [ "s3:AbortMultipartUpload", "s3:GetBucketLocation", "s3:GetObject", "s3:ListBucket", "s3:ListBucketMultipartUploads", "s3:PutObject", ], effect: iam.Effect.ALLOW, resources: [wafLogBucket.bucketArn, `${wafLogBucket.bucketArn}/*`], }) ); wafLogDeliveryStreamRole.addToPolicy( new iam.PolicyStatement({ actions: ["logs:PutLogEvents"], effect: iam.Effect.ALLOW, resources: [ `arn:aws:logs:${region}:${accountId}:log-group:/aws/kinesisfirehose/*`, ], }) ); //配信ストリーム new firehose.CfnDeliveryStream(this, "wafLogDeliveryStream", { deliveryStreamName: deliveryStreamName, deliveryStreamType: "DirectPut", s3DestinationConfiguration: { bucketArn: wafLogBucket.bucketArn, roleArn: wafLogDeliveryStreamRole.roleArn, cloudWatchLoggingOptions: { enabled: true, logGroupName: wafLogDeliveryStreamLogGroup.logGroupName, logStreamName: logStreamName, }, compressionFormat: "GZIP", prefix: "logs/", errorOutputPrefix: "errors/", }, }); } }
このCDKスタック中ではWebACLとDeliveryStreamの紐付けは行いません。@aws-cdk/aws-wafv2
のドキュメントを見る限り、紐付けるためのプロパティがなく出来ないためです。
よって紐付けの設定のみAWS CLIで手動で行います。
CDKデプロイ
% cdk deploy
WebACLとDeliveryStreamを紐付ける
まずWebACLとDeliveryStreamのArnを確認します。(CDKスタック内でconsole.log
でも確認可能です)
REGION=us-east-1 ACCOUNT_ID=XXXXXXXXXXXX % aws wafv2 list-web-acls \ --scope CLOUDFRONT \ --region ${REGION}
% aws firehose describe-delivery-stream \ --delivery-stream-name aws-waf-logs-demo \ --region ${REGION} \ --query DeliveryStreamDescription.DeliveryStreamARN
下記のコマンドで両者のArnを指定して紐付けます。
% aws wafv2 put-logging-configuration \ --logging-configuration \ ResourceArn=${WEB_ACL_ARN},LogDestinationConfigs=${FIREHOSE_DELIVERY_STREAM_ARN} \ --region ${REGION}
動作確認
WebACLが適用されているCloudFrontがホストしているWebサイトにアクセスします。
10分ほどしたらS3バケットのlogs
プレフィクス配下にログが作成されました。
% aws s3 ls s3://aws-waf-logs-demo-${REGION}-${ACCOUNT_ID} --recursive 2021-06-10 19:04:27 1559 logs/2021/06/10/09/aws-waf-logs-demo-1-2021-06-10-09-58-08-9864cb19-1d2e-4a57-9e30-0ddfdca6f5e3.gz
S3 SELECTを使用して下記のような指定でログの内容を確認できました。
- 入力設定
- 形式:JSON
- JSONコンテンツタイプ:行
- 圧縮:GZIP
- 出力設定
- 形式JSON
WebACLのログのフィールド仕様は下記ドキュメントから確認可能です。
Log Examples
{ "timestamp": 1576280412771, "formatVersion": 1, "webaclId": "arn:aws:wafv2:ap-southeast-2:EXAMPLE12345:regional/webacl/STMTest/1EXAMPLE-2ARN-3ARN-4ARN-123456EXAMPLE", "terminatingRuleId": "STMTest_SQLi_XSS", "terminatingRuleType": "REGULAR", "action": "BLOCK", "terminatingRuleMatchDetails": [ { "conditionType": "SQL_INJECTION", "location": "HEADER", "matchedData": [ "10", "AND", "1" ] } ], "httpSourceName": "-", "httpSourceId": "-", "ruleGroupList": [], "rateBasedRuleList": [], "nonTerminatingMatchingRules": [], "httpRequest": { "clientIp": "1.1.1.1", "country": "AU", "headers": [ { "name": "Host", "value": "localhost:1989" }, { "name": "User-Agent", "value": "curl/7.61.1" }, { "name": "Accept", "value": "*/*" }, { "name": "x-stm-test", "value": "10 AND 1=1" } ], "uri": "/foo", "args": "", "httpVersion": "HTTP/1.1", "httpMethod": "GET", "requestId": "rid" }, "labels": [ { "name": "value" } ] }
おわりに
AWS WAF v2のFirehoseを使用したS3へのログ出力をAWS CDK(一部AWS CLI)で設定してみました。
WebACLのログからアクセスのAllow/Block/Countの状況を確認して、ルールの最適化を行いたい場合にこの設定がされていると便利ですね。
参考
- Amazon S3 にログを保存するように AWS WAF の包括的なログ記録を設定する
- AWS CDKでAPI GatewayからKinesis Data Streamsにデータを流す設定を作成してみた | DevelopersIO
- AWS WAFのLogを保存するKinesis FirehoseをCloudFormationでさくっと作ってみた | DevelopersIO
以上